第一次看到 HAL(Hardware Abstraction Layer)是在 mruby/c 的原始碼裡面,我們可以透過編譯時提供 -DMRBC_USE_HAL_ESP32 來選擇要使用 ESP32 相關的硬體設置,而這些設置可能會影響如何做 STDOUT 或者其他處理。
看到這邊,上一篇對 mrb_puts 的實作似乎也是這樣沒錯
void mrb_puts(mrb_state* mrb) {
  int argc = mrb_get_argc(mrb);
  mrb_value* argv = mrb_get_argv(mrb);
#if defined(ESP8266)
  char num[4];
  tft.fillScreen(TFT_BLACK);
#endif
  for(int i = 0; i < argc; i++) {
    printf("%d\n", mrb_fixnum(argv[i]));
#if defined(ESP8266)
    sprintf(num, "%d", mrb_fixnum(argv[i]));
    tft.drawString(num, 8, i * 16 + 8, 1);
#endif
}
不同的是 mrubyc 是以檔案區分的,因此我們可以看到原始碼中會有 hal_esp32、hal_posix 等目錄,如果我們可以有這樣的配置,是不是就能將桌機跟開發版的程式碼實作區分開來了呢?
在經過一些嘗試之後,因為 Arduino 需要的 setup 和 loop 跟桌機使用的 main 差異,所以最後只能採取將原始碼完全分離的處理,不過我們的使用情境大多會落在 Arduino 的狀況下,之後只需要區分 Arduino 跟桌機的測試版,似乎也還在接受範圍內,理想狀況下我們應該是透過不同的 Header 定義將 API 分離開來,讓我們可以呼叫相同的方法但能夠針對不同硬體做出對應的處理。
下面是嘗試調整後的結構
├── include
│   ├── embed_methods.h
│   ├── hal.h
│   └── ruby.h
└── src
    ├── hal_arduino
    │   ├── embed_methods.cpp
    │   └── hal.h
    ├── hal_native
    │   ├── embed_methods.c
    │   └── hal.h
    └── main.cpp
首先我們先製作 include/hal.h 讓我們可以依照硬體類型選擇對應的 Header 作為參考使用。
#ifndef HAL_
#define HAL_
#if defined(ESP8266)
#include "hal_arduino/hal.h"
#else
#include "hal_native/hal.h"
#endif
#endif
然後再將 mrb_puts 定義到 include/embed_methods.h 方便其他實作可以參考到這個方法。
#ifndef EMBED_METHODS_
#define EMBED_METHODS_
#include <stdio.h>
#include "vm.h"
#include "iron.h"
#ifdef __cplusplus
extern "C" {
#endif
void mrb_puts(mrb_state* mrb);
#ifdef __cplusplus
}
#endif
#endif
另外因為 run_vm 也是共用的,所以改為 include/ruby.h 方便不同硬體呼叫。
#ifndef RUBY_
#define RUBY_
#include "app.h"
#include "vm.h"
#include "iron.h"
void bootstrap_ruby() {
  mrb_state* mrb = mrb_open();
  mrb_define_method(mrb, "puts", mrb_puts);
  mrb_run(mrb, app);
  mrb_close(mrb);
}
#endif
這邊可能還有其他更好的做法,不過因為是初次嘗試所以就先用相對單純的方式實現。
然後我們再根據不同的硬體時做對應的行為,像是 src/hal_arduino/hal.h
#ifndef ARDUINO_HAL_
#define ARDUINO_HAL_
#include <Arduino.h>
#include <TFT_eSPI.h>
#include "embed_methods.h"
static TFT_eSPI tft = TFT_eSPI();
#ifdef __cplusplus
extern "C" {
#endif
void setup();
void loop();
#ifdef __cplusplus
}
#endif
#endif
以及實作本身的 src/hal_arduino/embed_methods.cpp
#if defined(ESP8266)
#include "hal.h"
#include "ruby.h"
void mrb_puts(mrb_state* mrb) {
  int argc = mrb_get_argc(mrb);
  mrb_value* argv = mrb_get_argv(mrb);
  char num[4];
  tft.fillScreen(TFT_BLACK);
  for(int i = 0; i < argc; i++) {
    sprintf(num, "%d", mrb_fixnum(argv[i]));
    tft.drawString(num, 8, i * 16 + 8, 1);
  }
}
void setup() {
  Serial.begin(9600);
  tft.init();
  tft.setTextSize(1);
  tft.setTextColor(TFT_GREEN, TFT_BLACK);
  bootstrap_ruby();
}
void loop() {
}
#endif
因為 Arduino 的程式碼是以 C++ 為基礎,因此我們會需要用 cpp 作為副檔名,否則會無法編譯。
然後再針對桌機的版本實作 src/hal_native/hal.h
#ifndef NATIVE_HAL_
#define NATIVE_HAL_
#include "embed_methods.h"
int main(int argc, char** argv);
#endif
以及實際上的 mrb_puts 方法到 src/hal_native/embed_methods.c
#if !defined(ESP8266)
#include "hal.h"
#include "ruby.h"
void mrb_puts(mrb_state* mrb) {
  int argc = mrb_get_argc(mrb);
  mrb_value* argv = mrb_get_argv(mrb);
  for(int i = 0; i < argc; i++) {
    printf("%d\n", mrb_fixnum(argv[i]));
  }
}
int main(int argc, char** argv) {
  bootstrap_ruby();
  return 0;
}
#endif
最後我們的 src/main.cpp 就會剩下一個基本上是空的檔案
/**
 * iron-ruby-vm
 */
#include "hal.h"
雖然感覺很微妙而且應該還能有改進的空間,不過我們還是先往下一階段前進吧!